#include <string.h>
#include <sys/time.h>

#include <sqlite3.h>

#include <string>
#include <stdexcept>

#include <boost/cstdint.hpp>

#include "sqlite_cxx.hpp"

#include "pam_srp.hpp"

extern "C"
{
#include "srp.h"
}

// DEBUGGING
#include <iostream>
using namespace std;

namespace pam_srp
{    
    using namespace sqlite_cxx;

    void authenticate_from_cache( SRPSettings & settings,
                                  const char * username,
                                  const char * password )
    {
        SRPVerifier         * ver      = 0;
        SRPUser             * usr      = 0;
        SRP_HashAlgorithm     hash_alg = (SRP_HashAlgorithm) settings.hash_alg;
        SRP_NGType            ng_type  = (SRP_NGType       ) settings.ng_type;
    
        const unsigned char * bytes_s = 0;
        const unsigned char * bytes_v = 0;
        const unsigned char * bytes_A = 0;
        const unsigned char * bytes_B = 0;
    
        const unsigned char * bytes_M    = 0;
        const unsigned char * bytes_HAMK = 0;
        const char          * auth_username; // ignore
    
        int len_s   = 0;
        int len_v   = 0;
        int len_A   = 0;
        int len_B   = 0;
        int len_M   = 0;
        int ret     = 0;

 
        if (settings.cred_cache_file == "" || settings.max_cached_creds <= 0)
            throw AuthenticationFailed("Credentials Cache Disabled");
            
    
        try
        {
            DBConnection conn = open_database( settings.cred_cache_file );

            conn->set_busy_timeout( 500 ); // 1/2 second before error
        
            SQLStatement stmt = conn->sql( "SELECT salt,verifier FROM cache WHERE username = ?" );

            stmt->bind( 1, username );

            stmt->step(); // throws if busy (prevents locking up)
        
            if ( !stmt->have_data() )
                throw AuthenticationFailed("User not found in credential cache");
        
            bytes_s = (const unsigned char *)stmt->column_blob(0);
            bytes_v = (const unsigned char *)stmt->column_blob(1);

            len_s = stmt->column_bytes(0);
            len_v = stmt->column_bytes(1);
        
            usr =  srp_user_new( hash_alg, ng_type, username, 
                                 (const unsigned char *)password, 
                                 strlen(password), 0, 0 );
    
            srp_user_start_authentication( usr, &auth_username, &bytes_A, &len_A );
    
            ver =  srp_verifier_new( hash_alg, ng_type, username, bytes_s, len_s,
                                     bytes_v, len_v, bytes_A, len_A,
                                     &bytes_B, &len_B, 0, 0 );
    
            if ( bytes_B ) // SRP-6a safety check
            {
                /* Host -> User: (bytes_s, bytes_B) */
                srp_user_process_challenge( usr, bytes_s, len_s, bytes_B, len_B, &bytes_M, &len_M );
        
                if ( bytes_M ) // SRP-6a safety check
                {
            
                    /* User -> Host: (bytes_M) */
                    srp_verifier_verify_session( ver, bytes_M, &bytes_HAMK );
            
                    if ( !bytes_HAMK )
                        throw AuthenticationFailed("Invalid password");
                }
            }
        }
        catch( SQLiteError & e )
        {
            throw AuthenticationFailed( "SQLite error: %s",  e.what() );
        }
        catch( AuthenticationFailed )
        {
            if( ver ) srp_verifier_delete( ver );
            if( usr ) srp_user_delete( usr );

            throw;
        }
        
        if( ver ) srp_verifier_delete( ver );
        if( usr ) srp_user_delete( usr );
    }


    void cache_credentials( SRPSettings & settings,
                            const char * username,
                            const char * password,
                            const char **cache_error )
    {
        const char * errmsg = NULL;

        *cache_error = NULL;
    
        if (settings.cred_cache_file == "" || settings.max_cached_creds <= 0)
            return; // cached credentials not enabled
        

        // Check to see if the cache already contains a valid cached
        // credential for this username/password

        try
        {
            authenticate_from_cache( settings, username, password );
            return; // cache is up to date
        }
        catch( AuthenticationFailed )
        {} // cache update requried

        SRP_HashAlgorithm     hash_alg = (SRP_HashAlgorithm) settings.hash_alg;
        SRP_NGType            ng_type  = (SRP_NGType       ) settings.ng_type;
        const unsigned char * bytes_s = 0;
        const unsigned char * bytes_v = 0;
        int len_s   = 0;
        int len_v   = 0;

        timeval tv;

        gettimeofday( &tv, 0 );

        boost::int64_t now = tv.tv_sec * 1000000 + tv.tv_usec;
    
        srp_create_salted_verification_key( hash_alg, ng_type, username, 
                                            (const unsigned char *)password, 
                                            strlen(password), 
                                            &bytes_s, &len_s, &bytes_v, &len_v, 0, 0 );
        
        try
        {
        
            SQLStatement have, update, count, oldest, del, add;
        
            DBConnection conn = open_database( settings.cred_cache_file );

            conn->set_busy_timeout( 500 ); // 1/2 second before error
        
            have = conn->sql("SELECT salt FROM cache WHERE username = ?");

            have->bind(1, username);
        
            have->step();
        
            if ( have->have_data() ) // Entry already in cache
            {
                update = conn->sql("UPDATE cache SET salt=?, verifier=?, timestamp=? WHERE username = ?" );

                update->bind( 1, bytes_s, len_s );
                update->bind( 2, bytes_v, len_v );
                update->bind( 3, now );
                update->bind( 4, username );

                update->step();
            

                conn->sql("COMMIT")->step();            
            }
            else
            {
                // add new entry
                count = conn->sql( "SELECT COUNT(username) FROM cache" );
                count->step();

                if ( count->have_data() && count->column_int(0) >= settings.max_cached_creds )
                {                
                    oldest = conn->sql( "SELECT username FROM cache ORDER BY timestamp" );
                    del    = conn->sql( "DELETE FROM cache WHERE username = ?" );
                
                    // Delete oldest entries to make room for the new cred entry
                    do
                    {
                        oldest->reset();
                    
                        del->reset();
                        del->clear_bindings();

                        oldest->step();
                        del->bind( 1, (const char *)oldest->column_text(0) );

                        del->step();

                        // requery
                    
                        count->reset();
                        count->step();
                    }
                    while( count->have_data() && count->column_int(0) >= settings.max_cached_creds );
                }
            
                add = conn->sql( "INSERT INTO cache VALUES(?,?,?,?)" );
            
                add->bind(1, username);
                add->bind(2, bytes_s, len_s);
                add->bind(3, bytes_v, len_v);
                add->bind(4, now);
            
                add->step();
            
                conn->sql("COMMIT")->step();
            }
        }
        catch( SQLiteError & e )
        {
            *cache_error = e.what();
            cerr << "CACHE ERROR: " << *cache_error << endl;
            return;
        }
    }

}// namespace pam_srp
